from __future__ import absolute_import
from os.path import join, exists
import os

from forest.api import FOREST_HEADER
from forest.api import Command,  get_tool

NetworkDevices = get_tool('network.NetworkDevices')
NetworkDeviceMatcher = get_tool('network.NetworkDeviceMatcher')

class Network(Command):
    ''' Configures the network service or Network Manager.

        If using Network Manager just do::

            network:
                use_networkmanager: true


        An example of configuring all ethernet devices using dhcp::

            network:
              devices:
              - match: ['class=ethernet']
                bootproto: 'dhcp'

        A example of configuring specific devices::

            network:
              devices:
              # matches the first ethernet device
              - match: ['class=ethernet', 'order=1']
                ip: xxx.xxx.xxx.xxx
                netmask: xxx.xxx.xxx.xxx
                gateway: xxx.xxx.xxx.xxx

              - match: ['class=ethernet', 'order=2']
                bootproto: dhcp


        .. describe:: setup.network.devices

            A list of devices to match and their configuration.
            Each is a dictionary specifying how to configure the device.

        The mini expression language:

        * match: ['name=eth0'] or (not reliable)

        * match: ['address=xx:xx:xx:xx:xx:xx'] or (better)

        * match: ['name=(p6p|eth)\d+'] or (matching motherboard or extra ether)

        * match: ['class=(any|all|ethernet|wireless|...)'] or (wildcard, for
          workstations/easy dhcp)

        * match: ['class=ethernet', 'order=\d+'] (meaning 1st/2nd/3rd/etc of
          device class, can be somewhat reliable as some devices are checked
          first.. although mb manf has discretion)..
    '''
    __register__ = 'commands.setup.Network'

    requires_config = ['network']

    def run(self, config):
        self.args = config.get('network', False)

        if not self.args:
            return

        nm_targets = self.args.get('use_networkmanager', False)
        if nm_targets:
            self.enable_nm(config)
            return

        self.udev = []
        #installtree = config.get_install_root()
        devs = NetworkDevices()
        ndm = NetworkDeviceMatcher(devs)

        self.enable_network_service(config)
        specs = self.args.get('devices')
        for spec in specs:
            expressions = spec.get('match')
            matches = ndm.match(*expressions)

            if not matches:
                continue

            self.generate_and_write_ifconfigs(config, matches, spec)

            # TODO: generate the udev in order
            self.generate_udev(matches)

        self.write_udev(config)
        self.write_domain(config)

    @staticmethod
    def enable_nm(config):
        ''' Enables the NetworkManager service instead of any
        explicit device specifications.
        '''
        service_name = 'NetworkManager.service'
        nm_target_prefix = '/lib/systemd/system/{}'
        service_path = nm_target_prefix.format(service_name)

        installtree = config.get_install_root()
        with get_tool('system.chroot')(installtree):

            if not exists(service_path):
                raise Exception("ERR - NetworkManager is to be enabled"
                    ",yet isn't properly installed - missing {0}".format(service_path))

            systemd = get_tool('systemd.Systemd')('/')
            systemd.get_service(service_name).enable()

    @staticmethod
    def enable_network_service(config, levels = None):
        '''enables the network, using sysvinit scripts
        , in runlevels 2-5, inclusive'''

        if levels is None:
            levels = [2, 3, 4, 5]

        installtree = config.get_install_root()

        with get_tool('system.cd')(installtree):
            for elevel in levels:
                get_tool('systemd.sysvinit_service')('network', elevel)

    def write_domain(self, config):
        '''Dhclient would overwrite resolv.conf at boot. this tries to prevent
        such behavior iff domain is explicitly set. Would be nice not to even
        install dhclient if the comp is statically configured - but currently,
        minimal.tree does install it..
        Another option is chattr to make the file immutable (+i). but this
        looked like it works on a quick test, and rather wouldn't involve another
        tool if not needed.

        also note - any DNS1/DOMAIN settings in the ifcfg file WILL overwrite
        resolv.conf. Way to protect against that - only by not putting them in.'''
        installtree = config.get_install_root()
        resolv_path = join(installtree, 'etc', 'resolv.conf')
        resolv = {}
        maybe_domain = self.args.get('domain', None)
        maybe_dns = self.args.get('dns', [])

        if maybe_domain is None:
            return

        if maybe_dns == []:
            print("if you wish a static dns & domain configuration,"
                " you need to specify both!")
            return

        resolv['search'] = [maybe_domain]
        resolv['nameserver'] = maybe_dns

        resolv_lst = []
        for key, val in resolv.iteritems():
            if val is None:
                continue
            for valel in val:
                resolv_lst.append('{0} {1}\n'.format(key, valel))

        get_tool('files.write')(resolv_path, ''.join(resolv_lst))

        protect_resolv_path = join(installtree, 'etc', 'dhcp', 'dhclient-enter-hooks')
        #noop
        protect_sh = ["make_resolv_conf(){", ":", "}"]

        protect_str = ''.join([
            '#!/bin/sh\n',
            '\n'.join(protect_sh),
            '\n'])
        get_tool('files.write')(protect_resolv_path, protect_str)
        os.chmod(protect_resolv_path, 0755)


    def generate_udev(self, devices):
        '''crates fixed names for network devices, appends them into an
        object variable'''

        for dev in devices:
            dev_type = open('/sys/class/net/{}/type'.format(
                dev.get('name'))).read().strip()
            dev_id = open('/sys/class/net/{}/dev_id'.format(
                dev.get('name'))).read().strip()

            dev_str = ('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", '
                'ATTR{{address}}=="{0}", ATTR{{dev_id}}=="{dev_id}", '
                'ATTR{{type}}=="{dev_type}", KERNEL=="eth*", NAME="{1}"')
            self.udev.append(dev_str.format(dev.get('address'),
                dev.get('name'), dev_type = dev_type, dev_id = dev_id))

        return self.udev  # no such thing as just udev, no?

    def write_udev(self, config):
        '''writes out the found fixed names'''
        rules_path = config.join_install_root('etc', 'udev', 'rules.d', '70-persistent-net.rules')

        persistent_net_str = ''.join([
                FOREST_HEADER,
                '\n'.join(self.udev),
                '\n'])
        get_tool('files.write')(rules_path, persistent_net_str)

    @staticmethod
    def generate_and_write_ifconfigs(config, devices, configuration):
        '''getting biggish - should split in a couple of functions
        but not repeating the loop.
        name is self-descriptive
        '''
        installtree = config.get_install_root()
        ifcfg_root_path = config.join_install_root('etc', 'sysconfig', 'network-scripts')
        add_modconf = False
        bonding_dev = []

        #hostname = config.get('hostname', '#HOSTNAME#')
        for dev in devices:
            proto = configuration.get('bootproto', 'dhcp')
            master = configuration.get('master', None)
            bridge = configuration.get('bridge', None)
            address = configuration.get('address', None)

            ifcfg = {}
            ifcfg['BOOTPROTO'] = proto
            ifcfg['ONBOOT'] = configuration.get('onboot', 'yes')
            ifcfg['DEVICE'] = dev.get('name')
            ifcfg['USERCTL'] = configuration.get('userctl', 'no')
            #ifcfg['DHCP_HOSTNAME'] = hostname
            class_name = dev.get('class')
            if class_name == 'ethernet':
                class_name = 'Ethernet'
            ifcfg['TYPE'] = class_name
            if address is not None:
                ifcfg['HWADDR'] = address
            ifcfg['BONDING_OPTS'] = configuration.get('bonding_opts', None)

            if bridge is not None:
                ifcfg['BRIDGE'] = bridge

            if master is not None:
                ifcfg['MASTER'] = master
                ifcfg['SLAVE'] = configuration.get('slave', 'yes')
                add_modconf = True
                bonding_dev.append(master)

            if proto == 'static':
                ifcfg['IPADDR'] = configuration.get('ip')
                ifcfg['NETMASK'] = configuration.get('netmask')
                ifcfg['GATEWAY'] = configuration.get('gateway')

                #will this have any effect if domain is specified
                #it prob needs to modify the same file to work
                #if 'dns' in configuration:
                #    ifcfg['DNS1'] = configuration.get('dns')

                #if we ever wish to enable configuring it in ifcfg files:
                #if 'domain' in configuration:
                #    ifcfg['DOMAIN'] = configuration.get('domain', '')

            ifcfg_path = join(ifcfg_root_path, 'ifcfg-%s' % dev.get('name'))
            ifcfg_lst = [FOREST_HEADER]
            for key, val in ifcfg.iteritems():
                if val is None:
                    continue
                ifcfg_lst.append('{0}={1}\n'.format(key, val))
            get_tool('files.write')(ifcfg_path, ''.join(ifcfg_lst), mode='w')

        if add_modconf:
            #http://docs.fedoraproject.org/en-US/Fedora/16/html/System_Administrators_Guide/s2-networkscripts-interfaces-chan.html
            modconf_path = config.join_install_root('etc', 'modconf.d','bonding.conf')

            modconf_lst = []
            for dev in bonding_dev:
                modconf_lst.append('alias {} bonding\n'.format(dev))
            get_tool('files.write')(modconf_path, ''.join(modconf_lst))

